#=====================================================================================================
#Introduction:
#Inspired by 3b1b(Grant Sanderson), I made this Fourier rose code. Grant Sanderson is a very great person
#in teaching advanced mathematics. In his videos, he can always use animation to explain mathematical
#principles in simple terms and show the beauty of mathematics. In order to run this file and meet 
#your expectations, you need:
#-----------------------------------------------------------------------------------------------------
#1.basic python programming and cmd using skills
#2.install the manim animation engine in advance
#3.master the basic usage of manim
#4.a persistent heart
#
#-----------------------------------------------------------------------------------------------------
#To reapearance the Fourier Rose:
#1.after the installation of manim, cd to manim-master(or "manim")
#2.copy "assets" and "FourierRose.py" to manim-master, if there is a warning, choose "replace all".
#3.running the following: python3 -m manim FourierRose.py FourierRose -pl (the "-p" flag meaning the video
#will aytomatically open when it is done rendering, and the "-l" flag is for a faster rendering at a low
#quality. To get the higher quality video, just delete the "-l" flag)
#3.wait for rendering complete, the rest of time will be showed in the terminal.
#
#-----------------------------------------------------------------------------------------------------
#Where to download manim(old version):
#https://github.com/leekunhwee/manim . choose code➡️download ZIP➡️unzip it
#
#About the installation of manim(old version):
#refer to the "README.md" in the "manim-master" folder(or "manim" folder), or search online. 
#One important thing is there are 3 kinds of manim in the Internet. In this case, we use the one mentioned 
#above.
#
#About some helpful video tutorials:
#https://www.bilibili.com/video/BV1p54y197cC?share_source=copy_web
#https://www.bilibili.com/video/BV1kA411b7kq?share_source=copy_web
#https://www.bilibili.com/video/BV1vZ4y1x7hT?share_source=copy_web
#https://www.bilibili.com/video/BV1CC4y1H7kp?share_source=copy_web
#https://www.bilibili.com/video/BV1af4y1B7Xe?share_source=copy_web
#
#About the image interpretation of Fourier series(made by 3b1b):
#https://www.bilibili.com/video/av62763042
#
#About 3b1b's homepage in bilibili:
#https://space.bilibili.com/88461692?from=search&seid=2338386436074573367&spm_id_from=333.337.0.0
#
#The main part of this code is collated from this video, with my respectation:
#https://www.youtube.com/watch?v=2tTshwWTEic
#
#Have a good time.
#
#Author:                                 Version:                                 Date:
#Liwei Tang                              1.0.0                                    2022.2.17
#====================================================================================================

from manimlib.imports import *

class FourierCirclesScene(ZoomedScene):
    CONFIG = {
        "n_vectors": 10,
        "big_radius": 2,
        "colors": [
            BLUE_D,
            BLUE_C,
            BLUE_E,
            GREY_BROWN,
        ],
        "vector_config": {
            "buff": 0,
            "max_tip_length_to_length_ratio": 0.25,
            "tip_length": 0.15,
            "max_stroke_width_to_length_ratio": 10,
            "stroke_width": 1.7,
        },
        "circle_config": {
            "stroke_width": 1,
        },
        "base_frequency": 1,
        "slow_factor": 0.5,
        "center_point": ORIGIN,
        "parametric_function_step_size": 0.001,
        "drawn_path_color": YELLOW,
        "drawn_path_stroke_width": 2,
        "interpolate_config": [0, 1],
        # Zoom config
        "include_zoom_camera": False,
        "scale_zoom_camera_to_full_screen": False,
        "scale_zoom_camera_to_full_screen_at": 4,
        "zoom_factor": 0.3,
        "zoomed_display_height": 3,
        "zoomed_display_width": 4,
        "image_frame_stroke_width": 1,
        "zoomed_camera_config": {
            "default_frame_stroke_width": 3,
            "cairo_line_width_multiple": 0.05,
        },
        "zoom_position": lambda mob: mob.move_to(ORIGIN),
        "zoom_camera_to_full_screen_config": {
            "run_time": 3,
            "func": there_and_back_with_pause,
            "velocity_factor": 1
        },
        "wait_before_start": None
    }

    def setup(self):
        ZoomedScene.setup(self)
        self.slow_factor_tracker = ValueTracker(
            self.slow_factor
        )
        self.vector_clock = ValueTracker(0)
        self.add(self.vector_clock)

    def add_vector_clock(self):
        self.vector_clock.add_updater(
            lambda m, dt: m.increment_value(
                self.get_slow_factor() * dt
            )
        )

    def get_slow_factor(self):
        return self.slow_factor_tracker.get_value()

    def get_vector_time(self):
        return self.vector_clock.get_value()

    def get_freqs(self):
        n = self.n_vectors
        all_freqs = list(range(n // 2, -n // 2, -1))
        all_freqs.sort(key=abs)
        return all_freqs

    def get_coefficients(self):
        return [complex(0) for _ in range(self.n_vectors)]

    def get_color_iterator(self):
        return it.cycle(self.colors)

    def get_rotating_vectors(self, freqs=None, coefficients=None):
        vectors = VGroup()
        self.center_tracker = VectorizedPoint(self.center_point)

        if freqs is None:
            freqs = self.get_freqs()
        if coefficients is None:
            coefficients = self.get_coefficients()

        last_vector = None
        for freq, coefficient in zip(freqs, coefficients):
            if last_vector:
                center_func = last_vector.get_end
            else:
                center_func = self.center_tracker.get_location
            vector = self.get_rotating_vector(
                coefficient=coefficient,
                freq=freq,
                center_func=center_func,
            )
            vectors.add(vector)
            last_vector = vector
        return vectors

    def get_rotating_vector(self, coefficient, freq, center_func):
        vector = Vector(RIGHT, **self.vector_config)
        vector.scale(abs(coefficient))
        if abs(coefficient) == 0:
            phase = 0
        else:
            phase = np.log(coefficient).imag
        vector.rotate(phase, about_point=ORIGIN)
        vector.freq = freq
        vector.coefficient = coefficient
        vector.center_func = center_func
        vector.add_updater(self.update_vector)
        return vector

    def update_vector(self, vector, dt):
        time = self.get_vector_time()
        coef = vector.coefficient
        freq = vector.freq
        phase = np.log(coef).imag

        vector.set_length(abs(coef))
        vector.set_angle(phase + time * freq * TAU)
        vector.shift(vector.center_func() - vector.get_start())
        return vector

    def get_circles(self, vectors):
        return VGroup(*[
            self.get_circle(
                vector,
                color=color
            )
            for vector, color in zip(
                vectors,
                self.get_color_iterator()
            )
        ])

    def get_circle(self, vector, color=BLUE):
        circle = Circle(color=color, **self.circle_config)
        circle.center_func = vector.get_start
        circle.radius_func = vector.get_length
        circle.add_updater(self.update_circle)
        return circle

    def update_circle(self, circle):
        circle.set_width(2 * circle.radius_func())
        circle.move_to(circle.center_func())
        return circle

    def get_vector_sum_path(self, vectors, color=YELLOW):
        coefs = [v.coefficient for v in vectors]
        freqs = [v.freq for v in vectors]
        center = vectors[0].get_start()

        path = ParametricFunction(
            lambda t: center + reduce(op.add, [
                complex_to_R3(
                    coef * np.exp(TAU * 1j * freq * t)
                )
                for coef, freq in zip(coefs, freqs)
            ]),
            t_min=0,
            t_max=1,
            color=color,
            step_size=self.parametric_function_step_size,
        )
        return path

    def get_drawn_path_alpha(self):
        return self.get_vector_time()

    def get_drawn_path(self, vectors, stroke_width=None, **kwargs):
        if stroke_width is None:
            stroke_width = self.drawn_path_stroke_width
        path = self.get_vector_sum_path(vectors, **kwargs)
        broken_path = CurvesAsSubmobjects(path)
        broken_path.curr_time = 0
        start, end = self.interpolate_config

        def update_path(path, dt):
            alpha = self.get_drawn_path_alpha()
            n_curves = len(path)
            for a, sp in zip(np.linspace(0, 1, n_curves), path):
                b = (alpha - a)
                if b < 0:
                    width = 0
                else:
                    width = stroke_width * interpolate(start, end, (1 - (b % 1)))
                sp.set_stroke(width=width)
            path.curr_time += dt
            return path

        broken_path.set_color(self.drawn_path_color)
        broken_path.add_updater(update_path)
        return broken_path

    def get_y_component_wave(self,
                             vectors,
                             left_x=1,
                             color=PINK,
                             n_copies=2,
                             right_shift_rate=5):
        path = self.get_vector_sum_path(vectors)
        wave = ParametricFunction(
            lambda t: op.add(
                right_shift_rate * t * LEFT,
                path.function(t)[1] * UP
            ),
            t_min=path.t_min,
            t_max=path.t_max,
            color=color,
        )
        wave_copies = VGroup(*[
            wave.copy()
            for x in range(n_copies)
        ])
        wave_copies.arrange(RIGHT, buff=0)
        top_point = wave_copies.get_top()
        wave.creation = ShowCreation(
            wave,
            run_time=(1 / self.get_slow_factor()),
            rate_func=linear,
        )
        cycle_animation(wave.creation)
        wave.add_updater(lambda m: m.shift(
            (m.get_left()[0] - left_x) * LEFT
        ))

        def update_wave_copies(wcs):
            index = int(
                wave.creation.total_time * self.get_slow_factor()
            )
            wcs[:index].match_style(wave)
            wcs[index:].set_stroke(width=0)
            wcs.next_to(wave, RIGHT, buff=0)
            wcs.align_to(top_point, UP)
        wave_copies.add_updater(update_wave_copies)

        return VGroup(wave, wave_copies)

    def get_wave_y_line(self, vectors, wave):
        return DashedLine(
            vectors[-1].get_end(),
            wave[0].get_end(),
            stroke_width=1,
            dash_length=DEFAULT_DASH_LENGTH * 0.5,
        )

    def get_coefficients_of_path(self, path, n_samples=10000, freqs=None):
        if freqs is None:
            freqs = self.get_freqs()
        dt = 1 / n_samples
        ts = np.arange(0, 1, dt)
        samples = np.array([
            path.point_from_proportion(t)
            for t in ts
        ])
        samples -= self.center_point
        complex_samples = samples[:, 0] + 1j * samples[:, 1]

        return [
            np.array([
                np.exp(-TAU * 1j * freq * t) * cs
                for t, cs in zip(ts, complex_samples)
            ]).sum() * dt for freq in freqs
        ]

    def zoom_config(self):
        # This is not in the original version of the code.
        self.activate_zooming(animate=False)
        self.zoom_position(self.zoomed_display)
        self.zoomed_camera.frame.add_updater(lambda mob: mob.move_to(self.vectors[-1].get_end()))

    def scale_zoom_camera_to_full_screen_config(self):
        # This is not in the original version of the code.
        def fix_update(mob, dt, velocity_factor, dt_calculate):
            if dt == 0 and mob.counter == 0:
                rate = velocity_factor * dt_calculate
                mob.counter += 1
            else:
                rate = dt * velocity_factor
            if dt > 0:
                mob.counter = 0
            return rate

        fps = 1 / self.camera.frame_rate
        mob = self.zoomed_display
        mob.counter = 0
        velocity_factor = self.zoom_camera_to_full_screen_config["velocity_factor"]
        mob.start_time = 0
        run_time = self.zoom_camera_to_full_screen_config["run_time"]
        run_time *= 2
        mob_height = mob.get_height()
        mob_width = mob.get_width()
        mob_center = mob.get_center()
        ctx = self.zoomed_camera.cairo_line_width_multiple

        def update_camera(mob, dt):
            line = Line(
                mob_center,
                self.camera_frame.get_center()
            )
            mob.start_time += fix_update(mob, dt, velocity_factor, fps)
            if mob.start_time <= run_time:
                alpha = mob.start_time / run_time
                alpha_func = self.zoom_camera_to_full_screen_config["func"](alpha)
                coord = line.point_from_proportion(alpha_func)
                mob.set_height(
                    interpolate(
                        mob_height,
                        self.camera_frame.get_height(),
                        alpha_func
                    ),
                    stretch=True
                )
                mob.set_width(
                    interpolate(
                        mob_width,
                        self.camera_frame.get_width(),
                        alpha_func
                    ),
                    stretch=True
                )
                self.zoomed_camera.cairo_line_width_multiple = interpolate(
                    ctx,
                    self.camera.cairo_line_width_multiple,
                    alpha_func
                )
                mob.move_to(coord)
            return mob

        #self.zoomed_display.add_updater(update_camera)

class AbstractFourierOfTexSymbol(FourierCirclesScene):
    CONFIG = {
        "n_vectors": 50,
        "center_point": ORIGIN,
        "slow_factor": 1/60, #1 circle takes 1/slow_factor seconds
        "n_cycles": None,
        "run_time": 10,
        "tex": r"\rm M",
        "start_drawn": True,
        "path_custom_position": lambda mob: mob,
        "max_circle_stroke_width": 1,
        "tex_class": TexMobject,
        "tex_config": {
            "fill_opacity": 0,
            "stroke_width": 1,
            "stroke_color": WHITE
        },
        "include_zoom_camera": False,
        "scale_zoom_camera_to_full_screen": False,
        "scale_zoom_camera_to_full_screen_at": 1,
        "zoom_position": lambda mob: mob.scale(0.8).move_to(ORIGIN).to_edge(RIGHT)
    }

    def construct(self):
        # This is not in the original version of the code.
        #text1 = TextMobject("ROSE")
        #text1.scale(0.8)
        #text1.to_edge(DL)
        #text2 = TextMobject("Fourier-Transformation")
        #text2.scale(0.8)
        #text2.next_to([text1.get_corner(UP+RIGHT)+0.75*RIGHT], UP)

        #self.play([FadeIn(TextTittle)], *[FadeIn(TextTittle2)], run_time = 2)
        #self.play([FadeOut(TextTittle)], *[FadeOut(TextTittle2)], run_time = 2)

        self.add_vectors_circles_path()
        if self.wait_before_start != None:
            self.wait(self.wait_before_start)
        self.add_vector_clock()
        self.add(self.vector_clock)


        if self.include_zoom_camera:
            self.zoom_config()
        if self.scale_zoom_camera_to_full_screen:
            self.run_time -= self.scale_zoom_camera_to_full_screen_at
            self.wait(self.scale_zoom_camera_to_full_screen_at)
            self.scale_zoom_camera_to_full_screen_config()
        if self.n_cycles != None:
            if not self.scale_zoom_camera_to_full_screen:
                for n in range(self.n_cycles):
                   self.run_one_cycle()
            else:
                cycle = 1 / self.slow_factor
                total_time = cycle * self.n_cycles
                total_time -= self.scale_zoom_camera_to_full_screen_at
                self.wait(total_time)
        elif self.run_time != None:
            self.wait(self.run_time)

    def add_vectors_circles_path(self):
        path = self.get_path()
        self.path_custom_position(path)
        coefs = self.get_coefficients_of_path(path)
        vectors = self.get_rotating_vectors(coefficients=coefs)
        circles = self.get_circles(vectors)
        self.set_decreasing_stroke_widths(circles)
        drawn_path = self.get_drawn_path(vectors)
        if self.start_drawn:
            self.vector_clock.increment_value(1)
        self.add(path)
        self.add(vectors)
        self.add(circles)
        self.add(drawn_path)

        self.vectors = vectors
        self.circles = circles
        self.path = path
        self.drawn_path = drawn_path

    def run_one_cycle(self):
        time = 1 / self.slow_factor
        self.wait(time)

    def set_decreasing_stroke_widths(self, circles):
        mcsw = self.max_circle_stroke_width
        for k, circle in zip(it.count(1), circles):
            circle.set_stroke(width=max(
                mcsw / k,
                mcsw,
            ))
        return circles

    def get_path(self):
        tex_mob = self.tex_class(self.tex, **self.tex_config)
        tex_mob.set_height(6)
        path = tex_mob.family_members_with_points()[0]
        return path


class AbstractFourierFromSVG(AbstractFourierOfTexSymbol):
    CONFIG = {
        "n_vectors": 101,
        "run_time": 10,
        "start_drawn": True,
        "file_name": None,
        "svg_config": {
            "fill_opacity": 0,
            "stroke_color": WHITE,
            "stroke_width": 1,
            "height": 7
        }
    }

    def get_shape(self):
        shape = SVGMobject(self.file_name, **self.svg_config)
        return shape

    def get_path(self):
        shape = self.get_shape()
        path = shape.family_members_with_points()[0]
        return path



class FourierOfPaths(AbstractFourierOfTexSymbol):
    CONFIG = {
        "n_vectors": 100,
        "name_color": WHITE,
        "tex_class": TexMobject,
        "tex": None,
        "file_name": None,
        "tex_config": {
            "stroke_color": WHITE,
            "fill_opacity": 0,
            "stroke_width": 3,
        },
        "svg_config": {},
        "time_per_symbol": 5,
        "slow_factor": 1 / 5,
        "parametric_function_step_size": 0.01,
        "include_zoom_camera": False,
        "scale_zoom_camera_to_full_screen": False,
    }

    def construct(self):
        self.add_vector_clock()
        if self.tex != None:
            name = self.tex_class(self.tex, **self.tex_config)
        elif self.file_name != None and self.tex == None:
            name = SVGMobject(self.file_name, **self.svg_config)
        max_width = FRAME_WIDTH - 2
        max_height = FRAME_HEIGHT - 2
        name.set_width(max_width)
        if name.get_height() > max_height:
            name.set_height(max_height)

        frame = self.camera.frame
        frame.save_state()

        vectors = VGroup(VectorizedPoint())
        circles = VGroup(VectorizedPoint())
        for path in name.family_members_with_points():
            for subpath in path.get_subpaths():
                sp_mob = VMobject()
                sp_mob.set_points(subpath)
                coefs = self.get_coefficients_of_path(sp_mob)
                new_vectors = self.get_rotating_vectors(
                    coefficients=coefs
                )
                new_circles = self.get_circles(new_vectors)
                self.set_decreasing_stroke_widths(new_circles)

                drawn_path = self.get_drawn_path(new_vectors)
                drawn_path.clear_updaters()
                drawn_path.set_style(**self.tex_config)
                drawn_path.set_style(**self.svg_config)

                static_vectors = VMobject().become(new_vectors)
                static_circles = VMobject().become(new_circles)

                self.play(
                    Transform(vectors, static_vectors, remover=True),
                    Transform(circles, static_circles, remover=True),
                    frame.set_height, 1.5 * name.get_height(),
                    frame.move_to, path,
                )

                self.add(new_vectors, new_circles)
                self.vector_clock.set_value(0)
                self.play(
                    ShowCreation(drawn_path),
                    rate_func=linear,
                    run_time=self.time_per_symbol
                )
                self.remove(new_vectors, new_circles)
                self.add(static_vectors, static_circles)

                vectors = static_vectors
                circles = static_circles
        self.play(
            FadeOut(vectors),
            FadeOut(circles),
            Restore(frame),
            run_time=2
        )
        self.wait(3)


class FourierRose(AbstractFourierFromSVG):
    CONFIG = {
        "start_drawn": False,          # if start_drawn = True, the path start to draw
        "file_name": "flower11-2.svg", #Write your SVG file here, it should be placed in manim-master/assets/svg_images, if there is not a folder, then creat it.
        "svg_config": {
            "fill_opacity": 0,         #the opacity of the SVG file
            "stroke_color": WHITE,     #the color of the SVG file's edge
            "stroke_width": 1,
            "height": 7
        },
        # Draw config
        "drawn_path_color": RED,       #the color of drawn path
        "interpolate_config": [1, 1],  #the opacity of the drawn path at the beginning and the end, the number is from 0 to 1.
        "n_vectors": 75,              #the number of vectors, the bigger number you write here, the closer you draw the imagine to the source file.
        "big_radius": 2,
        "drawn_path_stroke_width": 2,
        "center_point": ORIGIN,
        # Duration config
        #"slow_factor": 0.1,           #"slow_factor" determines the playback rate of the Fourier animation, playback time = 1/"slow_factor"
        "n_cycles": 1,                 #"n_cycles" means the cycle the Fourier video will play, it cannot be defined with "run_time" at the same time. To slow the playback of the Fourier video, you can use "slow_factor".
        #"run_time": 10,
        # colors of circles
        "colors": [
            BLUE_D,
            BLUE_C,
            BLUE_E,
            GREY_BROWN,
        ],
        # circles config
        "circle_config": {
            "stroke_width": 1,
        },
        # vector config
        "vector_config": {
            "buff": 0,
            "max_tip_length_to_length_ratio": 0.25,
            "tip_length": 0.15,
            "max_stroke_width_to_length_ratio": 10,
            "stroke_width": 1.7,
        },
        "base_frequency": 1,
        # definition of subpaths
        "parametric_function_step_size": 0.001,
    }

class ShowTittle(Scene):
    def construct(self):
        textTitle = VGroup(Text("TLW Studio").scale(0.5))
        textTitle.scale(1.5)
        textmotto = TextMobject("“如果世上真的存在世外桃源，", "我想那里一定会有一片海，", "海鸥扑腾着翅膀,清风拂过浪花，日出的温暖刚刚好，", "你安静的坐在那儿，", "给世界一记温柔。”")
        textmotto.scale(0.7)
        textmotto3 = TextMobject("“我走过许多地方的桥，看过许多次数的云，喝过许多\\\\种类的酒，却只爱过一个正当最好年龄的人。”", "——沈从文")
        textmotto3[1].next_to(textmotto3[0], DR)
        textmotto3.scale(0.7)
        textmotto2 = TextMobject("谨以此片，", "致我此前、现在以及之后的所有青春，", "\\\\ ", "也致：")
        textmotto2.scale(0.7)
        textname = TextMobject("小情歌") 
        textname.scale(1.3)
        

        rectangle = Rectangle(
            width = 5,
            height = 2,
        )
        rectangle.set_fill(color = '#3eaeea', opacity = 1.0)
        rectangle.set_sheen(0.1, RIGHT)

        self.wait(0.5)

        self.play(Write(rectangle))
        self.play(*[Write(mob) for mob in textTitle])
        self.wait(2.5)
        self.play(FadeOut(rectangle), *[FadeOut(mob) for mob in textTitle])
        self.wait(1)

        self.play(Write(textmotto3[0]), run_time = 5)
        self.play(Write(textmotto3[1]), run_time = 0.7)
        self.wait(1.5)
        self.play(FadeOut(textmotto3)) 

        #self.play(Write(textmotto), run_time = 8)
        #self.wait(2)
        #self.play(FadeOut(textmotto))
        #self.wait(0.7)

        self.play(Write(textmotto2[0])) 
        self.play(Write(textmotto2[1]))
        self.play(Write(textmotto2[2]))
        self.play(Write(textmotto2[3]), run_time = 0.7)
        self.wait(1.5)
        self.play(FadeOut(textmotto2))

        
        
        self.play(DrawBorderThenFill(textname), run_time=8)
        #self.wait(0.7)
        self.play(FadeOut(textname))
        self.wait(1)

class ShowEnd(Scene):
    def construct(self):
    
        textend1 = TextMobject("祝天下有情人,终成眷属", run_time = 3)
        #ToDo textend1.scale = 0.?
        textend2 = TextMobject("—---汤力为")
        textend2.scale(0.6)
        textend2.to_edge(DR)
        textdate = TextMobject("22.1.27 to 22.2.14")
        textdate.scale(0.4)
        textdate.next_to(textend2,DOWN)
        
        self.play(Write(textend1), run_time = 3)
        self.wait(1.5)
        self.play(FadeOut(textend1))

        #self.play(Write(textend2), *[Write(textdate)])
        #self.play(Write(textdate))
        #self.wait(1)

        #self.play(FadeOut(textdate))
        #self.wait(0.5)

        #self.play(FadeOut(textend2), *[FadeOut(textdate)], run_time = 0.7)
        #self.play(FadeOut(textend2))
        self.wait(1)
